/*
 * Copyright 2008 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.oauth.http;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.oauth.client.ExcerptInputStream;
import net.oauth.OAuth;
import net.oauth.OAuthMessage;
import net.oauth.OAuthProblemException;
import net.oauth.ParameterStyle;

/**
 * An HTTP request or response.
 * 
 * @author John Kristian
 */
public class HttpMessage {

	public HttpMessage() {
		this(null, null);
	}

	public HttpMessage(String method, URL url) {
		this(method, url, null);
	}

	public HttpMessage(String method, URL url, InputStream body) {
		this.method = method;
		this.url = url;
		this.body = body;
	}

	public String method;
	public URL url;
	public final List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>();
	protected InputStream body = null;

	/**
	 * Get the value of the last header of the given name. The name is
	 * case-insensitive.
	 */
	public final String getHeader(String name) {
		String value = null;
		for (Map.Entry<String, String> header : headers) {
			if (equalsIgnoreCase(name, header.getKey())) {
				value = header.getValue();
			}
		}
		return value;
	}

	/**
	 * Remove all headers of the given name. The name is case insensitive.
	 * 
	 * @return the value of the last header with that name, or null to indicate
	 *         there was no such header
	 */
	public String removeHeaders(String name) {
		String value = null;
		for (Iterator<Map.Entry<String, String>> i = headers.iterator(); i
				.hasNext();) {
			Map.Entry<String, String> header = i.next();
			if (equalsIgnoreCase(name, header.getKey())) {
				value = header.getValue();
				i.remove();
			}
		}
		return value;
	}

	public final String getContentCharset() {
		return getCharset(getHeader(CONTENT_TYPE));
	}

	public final InputStream getBody() throws IOException {
		if (body == null) {
			InputStream raw = openBody();
			if (raw != null) {
				body = new ExcerptInputStream(raw);
			}
		}
		return body;
	}

	protected InputStream openBody() throws IOException {
		return null;
	}

	/** Put a description of this message and its origins into the given Map. */
	public void dump(Map<String, Object> into) throws IOException {
	}

	/**
	 * Construct an HTTP request from this OAuth message.
	 * 
	 * @param style
	 *            where to put the OAuth parameters, within the HTTP request
	 */
	public static HttpMessage newRequest(OAuthMessage from, ParameterStyle style)
			throws IOException {
		final boolean isPost = OAuthMessage.POST.equalsIgnoreCase(from.method);
		InputStream body = from.getBodyAsStream();
		if (style == ParameterStyle.BODY && !(isPost && body == null)) {
			style = ParameterStyle.QUERY_STRING;
		}
		String url = from.URL;
		final List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>(
				from.getHeaders());
		switch (style) {
		case QUERY_STRING:
			url = OAuth.addParameters(url, from.getParameters());
			break;
		case BODY: {
			byte[] form = OAuth.formEncode(from.getParameters()).getBytes(
					from.getBodyEncoding());
			headers.add(new OAuth.Parameter(CONTENT_TYPE, OAuth.FORM_ENCODED));
			headers.add(new OAuth.Parameter(CONTENT_LENGTH, form.length + ""));
			body = new ByteArrayInputStream(form);
			break;
		}
		case AUTHORIZATION_HEADER:
			headers.add(new OAuth.Parameter("Authorization", from
					.getAuthorizationHeader(null)));
			// Find the non-OAuth parameters:
			List<Map.Entry<String, String>> others = from.getParameters();
			if (others != null && !others.isEmpty()) {
				others = new ArrayList<Map.Entry<String, String>>(others);
				for (Iterator<Map.Entry<String, String>> p = others.iterator(); p
						.hasNext();) {
					if (p.next().getKey().startsWith("oauth_")) {
						p.remove();
					}
				}
				// Place the non-OAuth parameters elsewhere in the request:
				if (isPost && body == null) {
					byte[] form = OAuth.formEncode(others).getBytes(
							from.getBodyEncoding());
					headers.add(new OAuth.Parameter(CONTENT_TYPE,
							OAuth.FORM_ENCODED));
					headers.add(new OAuth.Parameter(CONTENT_LENGTH, form.length
							+ ""));
					body = new ByteArrayInputStream(form);
				} else if (isPost && body != null) {
					headers.add(new OAuth.Parameter(CONTENT_TYPE,
							"application/json"));
					url = OAuth.addParameters(url, others);
				} else {
					url = OAuth.addParameters(url, others);
				}
			}
			break;
		}
		HttpMessage httpRequest = new HttpMessage(from.method, new URL(url),
				body);
		httpRequest.headers.addAll(headers);
		return httpRequest;
	}

	private static boolean equalsIgnoreCase(String x, String y) {
		if (x == null)
			return y == null;
		else
			return x.equalsIgnoreCase(y);
	}

	private static final String getCharset(String mimeType) {
		if (mimeType != null) {
			Matcher m = CHARSET.matcher(mimeType);
			if (m.find()) {
				String charset = m.group(1);
				if (charset.length() >= 2 && charset.charAt(0) == '"'
						&& charset.charAt(charset.length() - 1) == '"') {
					charset = charset.substring(1, charset.length() - 1);
					charset = charset.replace("\\\"", "\"");
				}
				return charset;
			}
		}
		return DEFAULT_CHARSET;
	}

	/** The name of a dump entry whose value is the HTTP request. */
	public static final String REQUEST = OAuthProblemException.HTTP_REQUEST;

	/** The name of a dump entry whose value is the HTTP response. */
	public static final String RESPONSE = OAuthProblemException.HTTP_RESPONSE;

	/** The name of a dump entry whose value is the HTTP status code. */
	public static final String STATUS_CODE = OAuthProblemException.HTTP_STATUS_CODE;

	public static final String ACCEPT_ENCODING = "Accept-Encoding";
	public static final String CONTENT_ENCODING = "Content-Encoding";
	public static final String CONTENT_LENGTH = "Content-Length";
	public static final String CONTENT_TYPE = "Content-Type";
	public static final String DEFAULT_CHARSET = "UTF-8";

	private static final Pattern CHARSET = Pattern
			.compile("; *charset *= *([^;\"]*|\"([^\"]|\\\\\")*\")(;|$)");

}
